#!/bin/sh

# ctdbd wrapper - start or stop CTDB

usage ()
{
    echo "usage: ctdbd_wrapper <pidfile> { start | stop }"
    exit 1
}

[ $# -eq 2 ] || usage

pidfile="$1"
action="$2"

############################################################

if [ -z "$CTDB_BASE" ] ; then
    export CTDB_BASE="/usr/local/etc/ctdb"
fi

. "${CTDB_BASE}/functions"
loadconfig "ctdb"

[ -n "$CTDB_SOCKET" ] && export CTDB_SOCKET

ctdbd="${CTDBD:-/usr/local/sbin/ctdbd}"

############################################################

# ctdbd_is_running()

# 1. Check if ctdbd is running.
#    - If the PID file is being used then, if the PID file is present,
#      ctdbd is only considered to running if the PID in the file is
#      active.
#    - If the PID file is not being used (i.e. we're upgrading from a
#      version that doesn't support it) then the presence of any ctdbd
#      processes is enough proof.

# 2. Print a comma-separated list of PIDs that can be
#    used with "pkill -s".
#    - If the PID file is being used then this is just the PID in that
#      file.  This also happens to be the session ID, so can be used
#      to kill all CTDB processes.
#    - If the PID file is not being used (i.e. upgrading) then this is
#      just any ctdbd processes that are running.  Hopefully one of
#      them is the session ID so that it can be used to kill all CTDB
#      processes.

# Combining these 2 checks is an optimisation to avoid potentially
# running too many pgrep/pkill processes on an already loaded system.
# Trawling through /proc/ can be very expensive.

ctdbd_is_running ()
{
    # If the directory for the PID file exists then respect the
    # existence of a PID file.
    _pidfile_dir=$(dirname "$pidfile")
    if [ -d "$_pidfile_dir" ] ; then
	if read _pid 2>/dev/null <"$pidfile" ; then
	    echo "$_pid"

	    # Return value of kill is used
	    kill -0 "$_pid" 2>/dev/null
	else
	    # Missing/empty PID file
	    return 1
	fi
    else
	if _pid=$(pgrep -f "${ctdbd}\>") ; then
		# Use word splitting to squash whitespace
		# shellcheck disable=SC2086
	    echo $_pid | sed -e 's@ @,@g'
	    return 0
	else
	    return 1
	fi
    fi
}

############################################################

# If necessary, mount volatile database directory on tmpfs
dbdir_tmpfs_start ()
{
    if [ -z "$CTDB_DBDIR_TMPFS_OPTIONS" ] ; then
	return
    fi

    # Shortcut for readability
    _opts="$CTDB_DBDIR_TMPFS_OPTIONS"

    mkdir -p "$CTDB_DBDIR" || exit $?

    # If already mounted then remount, otherwise mount
    if findmnt -t tmpfs "$CTDB_DBDIR" >/dev/null ; then
	mount -t tmpfs -o "remount,$_opts" none "$CTDB_DBDIR" || \
	    exit $?
    else
	mount -t tmpfs -o "$_opts" none "$CTDB_DBDIR" || exit $?
    fi
}

# If necessary, unmount volatile database tmpfs directory on exit
dbdir_tmpfs_stop ()
{
    if [ -z "$CTDB_DBDIR_TMPFS_OPTIONS" ] ; then
	return
    fi

    if [ -d "$CTDB_DBDIR" ] && findmnt -t tmpfs "$CTDB_DBDIR" >/dev/null ; then
	umount "$CTDB_DBDIR"
    fi
}

# Only the nested function references its arguments
# shellcheck disable=SC2120
build_ctdb_options ()
{
    ctdb_options=""

    maybe_set ()
    {
	# If the given variable isn't set then do nothing
	[ -n "$2" ] || return
	# If a required value for the variable and it doesn't match,
	# then do nothing
	[ -z "$3" -o "$3" = "$2" ] || return

	val="'$2'"
	case "$1" in
	    --*) sep="=" ;;
	    -*)  sep=" " ;;
	esac
	# For these options we're only passing a value-less flag.
	if [ -n "$3" ] ; then
	    val=""
	    sep=""
	fi

	ctdb_options="${ctdb_options}${ctdb_options:+ }${1}${sep}${val}"
    }

    if [ -z "$CTDB_RECOVERY_LOCK" ] ; then
        echo "No recovery lock specified. Starting CTDB without split brain prevention."
    fi
    maybe_set "--reclock"                "$CTDB_RECOVERY_LOCK"

    maybe_set "--pidfile"                "$pidfile"

    # build up ctdb_options variable from optional parameters
    maybe_set "--logging"                "$CTDB_LOGGING"
    maybe_set "--nlist"                  "$CTDB_NODES"
    maybe_set "--socket"                 "$CTDB_SOCKET"
    maybe_set "--listen"                 "$CTDB_NODE_ADDRESS"
    maybe_set "--public-addresses"       "$CTDB_PUBLIC_ADDRESSES"
    maybe_set "--public-interface"       "$CTDB_PUBLIC_INTERFACE"
    maybe_set "--dbdir"                  "$CTDB_DBDIR"
    maybe_set "--dbdir-persistent"       "$CTDB_DBDIR_PERSISTENT"
    maybe_set "--dbdir-state"            "$CTDB_DBDIR_STATE"
    maybe_set "--event-script-dir"       "$CTDB_EVENT_SCRIPT_DIR"
    maybe_set "--transport"              "$CTDB_TRANSPORT"
    maybe_set "-d"                       "$CTDB_DEBUGLEVEL"
    maybe_set "--notification-script"    "$CTDB_NOTIFY_SCRIPT"
    maybe_set "--start-as-disabled"      "$CTDB_START_AS_DISABLED"    "yes"
    maybe_set "--start-as-stopped "      "$CTDB_START_AS_STOPPED"     "yes"
    maybe_set "--no-recmaster"           "$CTDB_CAPABILITY_RECMASTER" "no"
    maybe_set "--no-lmaster"             "$CTDB_CAPABILITY_LMASTER"   "no"
    maybe_set "--nosetsched"             "$CTDB_NOSETSCHED"           "yes"
    maybe_set "--script-log-level"       "$CTDB_SCRIPT_LOG_LEVEL"
    maybe_set "--max-persistent-check-errors" "$CTDB_MAX_PERSISTENT_CHECK_ERRORS"
}

export_debug_variables ()
{
    [ -n "$CTDB_DEBUG_HUNG_SCRIPT" ] && export CTDB_DEBUG_HUNG_SCRIPT
    [ -n "$CTDB_EXTERNAL_TRACE" ] && export CTDB_EXTERNAL_TRACE
    [ -n "$CTDB_DEBUG_LOCKS" ] && export CTDB_DEBUG_LOCKS
}

kill_ctdbd ()
{
    _session="$1"

    if [ -n "$_session" ] ; then
	pkill -9 -s "$_session" 2>/dev/null
    fi
}

############################################################

start()
{
    if _session=$(ctdbd_is_running) ; then
	echo "CTDB is already running"
	return 0
    fi

    # About to start new $ctdbd.  The main daemon is not running but
    # there may still be other processes around, so do some cleanup.
    kill_ctdbd "$_session"

    dbdir_tmpfs_start

    # build_ctdb_options() takes no arguments
    # shellcheck disable=SC2119
    build_ctdb_options

    export_debug_variables

    # Explicitly trying to disable core files, no other way
    # shellcheck disable=SC2039
    if [ "$CTDB_SUPPRESS_COREFILE" = "yes" ]; then
	ulimit -c 0
    else
	ulimit -c unlimited
    fi

    # Unsupported option easily avoided by not using configuration variable
    # shellcheck disable=SC2039
    if [ -n "$CTDB_MAX_OPEN_FILES" ]; then
	ulimit -n "$CTDB_MAX_OPEN_FILES"
    fi

    _d=$(dirname "$pidfile")
    mkdir -p "$_d"

    if [ -n "$CTDB_VALGRIND" -a "$CTDB_VALGRIND" != "no" ] ; then
	if [ "$CTDB_VALGRIND" = "yes" ] ; then
	    ctdbd="valgrind -q --log-file=/usr/local/var/log/ctdb_valgrind ${ctdbd}"
	else
	    ctdbd="${CTDB_VALGRIND} ${ctdbd}"
	fi
	ctdb_options="${ctdb_options} --valgrinding"
    fi

    case "$CTDB_LOGGING" in
	syslog:udp|syslog:udp-rfc5424)
	    logger -t ctdbd "CTDB is being run with ${CTDB_LOGGING}.  If nothing is logged then check your syslogd configuration"
	    ;;
	syslog|syslog:*) : ;;
	file:*)
	    logger -t ctdbd "CTDB is being run without syslog enabled.  Logs will be in ${CTDB_LOGGING#file:}"
	    ;;
	*)
	    logger -t ctdbd "CTDB is being run without syslog enabled.  Logs will be in log.ctdb"
    esac

    eval "$ctdbd" "$ctdb_options" || return 1

    # Wait until ctdbd has started and is ready to respond to clients.
    _pid=""
    _timeout="${CTDB_STARTUP_TIMEOUT:-10}"
    _count=0
    while [ "$_count" -lt "$_timeout" ] ; do
	# If we don't have the PID then try to read it.
	[ -n "$_pid" ] || read _pid 2>/dev/null <"$pidfile"

	# If we got the PID but the PID file has gone or the process
	# is no longer running then stop waiting... CTDB is dead.
	if [ -n "$_pid" ] ; then
	    if [ ! -e "$pidfile" ] || ! kill -0 "$_pid" 2>/dev/null ; then
		echo "CTDB exited during initialisation - check logs."
		kill_ctdbd "$_pid"
		drop_all_public_ips >/dev/null 2>&1
		return 1
	    fi

	    if $CTDB runstate first_recovery startup running >/dev/null 2>&1 ; then
		return 0
	    fi
	fi

	_count=$((_count + 1))
	sleep 1
    done

    echo "Timed out waiting for initialisation - check logs - killing CTDB"
    kill_ctdbd "$_pid"
    drop_all_public_ips >/dev/null 2>&1
    return 1
}

stop()
{
    if ! _session=$(ctdbd_is_running) ; then
	echo "CTDB is not running"
	return 0
    fi

    $CTDB shutdown

    # Wait for remaining CTDB processes to exit...
    _timeout=${CTDB_SHUTDOWN_TIMEOUT:-30}
    _count=0
    _terminated=false
    while [ "$_count" -lt "$_timeout" ] ; do
	if ! pkill -0 -s "$_session" 2>/dev/null ; then
	    _terminated=true
	    break
	fi

	_count=$((_count + 1))
	sleep 1
    done

    if ! $_terminated ; then
	echo "Timed out waiting for CTDB to shutdown.  Killing CTDB processes."
	kill_ctdbd "$_session"
	drop_all_public_ips >/dev/null 2>&1

	sleep 1

	if pkill -0 -s "$_session" ; then
	    # If SIGKILL didn't work then things are bad...
	    echo "Failed to kill all CTDB processes.  Giving up."
	    return 1
	fi
    fi

    dbdir_tmpfs_stop

    return 0
}

############################################################

# Allow notifications for start/stop.
if [ -x "$CTDB_BASE/rc.ctdb" ] ; then
    "$CTDB_BASE/rc.ctdb" "$action"
fi

case "$action" in
    start) start ;;
    stop)  stop  ;;
    *)
	echo "usage: $0 {start|stop}"
	exit 1
esac
